/*MyPhotoIndex Free Photo Organizer <www.myphotoindex.com>
  Copyright (C) <2006-2009>  <Asaf Yarkoni asaf@yarkoni.net>

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Drawing;
using System.Drawing.Imaging;
using System.Collections;
using System.IO;
using MyPhotoIndex.Utilities.Properties;
using System.Text;
using MyPhotoIndex.Interfaces;
using MyPhotoIndex.Interfaces.Imaging;

namespace MyPhotoIndex.Utilities.ImageMetaData
{
    public class ExifExtractor : IImageMetaData
    {
        private string m_imageDescription;
        private string m_cameraMaker;
        private string m_cameraModel;
        private int m_xResolution;
        private int m_yResolution;
        private ImageResolutionUnit m_resolutionUnit;
        private string m_softwareUsed;
        private DateTime m_lastModifiedDateTime;
        private String m_exposureTime;
        private float m_aperture;
        private float m_maxAperture;
        private CameraExposureProgram m_exposureProgram;
        private int m_isoSpeed;
        private DateTime m_dateTaken;
        private DateTime m_dateDigitized;
        private float m_brightness;
        private int m_exposureCompensation;
        private CameraMeteringMode m_meteringMode;
        private ImageLightSource m_lightSource;
        private CameraFlashStatus m_flashStatus;
        private float m_focalLength;
        private Byte[] m_thumbnail;
        private int m_imageWidth;
        private int m_imageHeight;
        private int m_thumbnailWidth;
        private int m_thumbnailHeight;
        private int m_orientation;
        private string m_sourceLabel;

        private IPTC m_iptc;


        #region IImageMetaData Members


        public List<string> Keywords
        {
            get
            {
                if (m_iptc == null)
                {
                    return new List<string>();
                }

                return m_iptc.Keywords;
            }
            set
            {
                if (m_iptc != null)
                {
                    m_iptc.Keywords = value;
                }
            }
        }

        #endregion

        #region properties

        public int Orientation
        {
            get { return m_orientation; }
            set { m_orientation = value; }
        }


        public string ImageDescription
        {
            get
            {
                if (m_iptc != null &&
                String.IsNullOrEmpty(m_iptc.Description) == false)
                {
                    return m_iptc.Description;
                }
                else
                {
                    return m_imageDescription;
                }
            }
            set { m_imageDescription = value; }
        }

        public string CameraMaker
        {
            get { return m_cameraMaker; }
            set { m_cameraMaker = value; }
        }

        public string CameraModel
        {
            get { return m_cameraModel; }
            set { m_cameraModel = value; }
        }

        public int XResolution
        {
            get { return m_xResolution; }
            set { m_xResolution = value; }
        }

        public int YResolution
        {
            get { return m_yResolution; }
            set { m_yResolution = value; }
        }

        public ImageResolutionUnit ResolutionUnit
        {
            get { return m_resolutionUnit; }
            set { m_resolutionUnit = value; }
        }

        public string SoftwareUsed
        {
            get { return m_softwareUsed; }
            set { m_softwareUsed = value; }
        }

        public DateTime LastModifiedDateTime
        {
            get { return m_lastModifiedDateTime; }
            set { m_lastModifiedDateTime = value; }
        }

        public String ExposureTime
        {
            get { return m_exposureTime; }
            set { m_exposureTime = value; }
        }

        public float Aperture
        {
            get { return m_aperture; }
            set { m_aperture = value; }
        }

        public float MaxAperture
        {
            get { return m_maxAperture; }
            set { m_maxAperture = value; }
        }

        public CameraExposureProgram ExposureProgram
        {
            get { return m_exposureProgram; }
            set { m_exposureProgram = value; }
        }

        public int IsoSpeed
        {
            get { return m_isoSpeed; }
            set { m_isoSpeed = value; }
        }

        public DateTime DateTaken
        {
            get { return m_dateTaken; }
            set { m_dateTaken = value; }
        }

        public DateTime DateDigitized
        {
            get { return m_dateDigitized; }
            set { m_dateDigitized = value; }
        }

        public float Brightness
        {
            get { return m_brightness; }
            set { m_brightness = value; }
        }

        public int ExposureCompensation
        {
            get { return m_exposureCompensation; }
            set { m_exposureCompensation = value; }
        }

        public CameraMeteringMode MeteringMode
        {
            get { return m_meteringMode; }
            set { m_meteringMode = value; }
        }

        public ImageLightSource LightSource
        {
            get { return m_lightSource; }
            set { m_lightSource = value; }
        }

        public CameraFlashStatus FlashStatus
        {
            get { return m_flashStatus; }
            set { m_flashStatus = value; }
        }

        public float FocalLength
        {
            get { return m_focalLength; }
            set { m_focalLength = value; }
        }

        public Byte[] Thumbnail
        {
            get { return m_thumbnail; }
            set { m_thumbnail = value; }
        }

        public int ImageWidth
        {
            get { return m_imageWidth; }
            set { m_imageWidth = value; }
        }

        public int ImageHeight
        {
            get { return m_imageHeight; }
            set { m_imageHeight = value; }
        }

        public int ThumbnailWidth
        {
            get { return m_thumbnailWidth; }
            set { m_thumbnailWidth = value; }
        }

        public int ThumbnailHeight
        {
            get { return m_thumbnailHeight; }
            set { m_thumbnailHeight = value; }
        }

        public string SourceLabel
        {
            get { return m_sourceLabel; }
            set { m_sourceLabel = value; }
        }

        #endregion


        public ExifExtractor(Image image)
        {
            if (image != null)
            {
                buildDB(image.PropertyItems);
            }
        }

        public ExifExtractor(string fileName)
        {
            if (string.IsNullOrEmpty(fileName) == true ||
                File.Exists(fileName) == false)
            {
                return;
            }

            try
            {
                DriveInfo driveInfo = new DriveInfo(fileName.Substring(0, 1));
                m_sourceLabel = driveInfo.VolumeLabel;
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }

            try
            {
                using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    using (Image image = Image.FromStream(fs, false, false))
                    {
                        m_imageWidth = image.Width;
                        m_imageHeight = image.Height;

                        buildDB(image.PropertyItems);
                    }
                }

                m_iptc = new IPTC(fileName);
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }
        }

        private void buildDB(PropertyItem[] parr)
        {
            Encoding ascii = Encoding.ASCII;
            foreach (PropertyItem p in parr)
            {
                //   ExifTranslation etl = new ExifTranslation();

                //string name = string.Empty;
                //etl.TryGetValue(p.Id, out name); ;

                if (p.Type == 0x1)//1 = BYTE An 8-bit unsigned integer.,
                {
                    if (p.Id == 20507)
                    {
                        m_thumbnail = p.Value;//thumbnail data == 501B
                    }
                    else
                    {
                        //ProcessMakerNote(p.Value);
                    }
                }
                else
                    if (p.Type == 0x2)//2 = ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.,
                    {
                        string stringValue = string.Empty;
                        if (p.Value != null)
                        {
                            stringValue = ascii.GetString(p.Value);
                        }

                        stringValue = stringValue.Replace("\0", "").Trim();
                        stringValue = stringValue.TrimEnd(new char[] { (char)0, (char)1, (char)2, (char)3 });
                        switch (p.Id)
                        {
                            case 0x10E: //Image Description
                                m_imageDescription = stringValue;
                                break;
                            case 0x10F: //"Equip Make"
                                m_cameraMaker = stringValue;
                                break;
                            case 0x110: //"Equip Model"
                                m_cameraModel = stringValue;
                                break;
                            case 0x131: //Software Used
                                m_softwareUsed = stringValue;
                                break;
                            case 0x132: //LastModified dateTime  
                                m_lastModifiedDateTime = GetDate(stringValue);
                                break;
                            case 0x9003: //DateTimeOriginal
                                m_dateTaken = GetDate(stringValue);
                                break;
                            case 0x9004: //DateTime Digitized

                                m_dateDigitized = GetDate(stringValue);

                                break;
                        }
                    }
                    else
                        if (p.Type == 0x3) //3 = SHORT A 16-bit (2 -byte) unsigned integer,
                        {
                            int value = (int)convertToInt16U(p.Value);
                            switch (p.Id)
                            {
                                case 0x128: //Resolution Unit
                                    m_resolutionUnit = (ImageResolutionUnit)value;
                                    break;
                                case 0x8822: //Exposure Prog 
                                    m_exposureProgram = (CameraExposureProgram)value;
                                    break;
                                case 0x8827: //ISO
                                    m_isoSpeed = value;
                                    break;
                                case 0x9207: // metering mode
                                    m_meteringMode = (CameraMeteringMode)value;
                                    break;
                                case 0x9208: // light source
                                    m_lightSource = (ImageLightSource)value;
                                    break;
                                case 0x9209: // Flash
                                    m_flashStatus = (CameraFlashStatus)value;
                                    break;
                                case 0x112: //Orientation
                                    m_orientation = value;
                                    break;
                            }
                        }
                        else
                            if (p.Type == 0x4)//4 = LONG A 32-bit (4 -byte) unsigned integer,
                            {
                                //switch (p.Id)
                                //{

                                //}
                                // orientation // lookup table					
                                // v = convertToInt32U(p.Value).ToString();
                            }

                            else if (p.Type == 0x5)//5 = RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the//denominator.,
                            {
                                byte[] n = new byte[p.Len / 2];
                                byte[] d = new byte[p.Len / 2];
                                Array.Copy(p.Value, 0, n, 0, p.Len / 2);
                                Array.Copy(p.Value, p.Len / 2, d, 0, p.Len / 2);
                                uint a = convertToInt32U(n);
                                uint b = convertToInt32U(d);
                                Rational r = new Rational(a, b);

                                switch (p.Id)
                                {
                                    case 0x11A: //X Resolution
                                        m_xResolution = (int)a;
                                        break;
                                    case 0x11B: //Y Resolution
                                        m_yResolution = (int)a;
                                        break;
                                    case 0x829A: //Exposure Time
                                        m_exposureTime = r.ToString(null);
                                        break;
                                    case 0x829D: // F-number
                                        m_aperture = (float)r.ToDouble();
                                        break;
                                    case 0x9205: // MaxAperture
                                        m_maxAperture = (float)r.ToDouble();
                                        break;
                                    case 0x920A: //FocalLength
                                        m_focalLength = (float)r.ToDouble();
                                        break;
                                }
                            }
                            else
                                if (p.Type == 0x7)//7 = UNDEFINED An 8-bit byte that can take any value depending on the field definition,
                                {
                                    //   ProcessMakerNote(p.Value);
                                    //switch (p.Id)
                                    //{
                                    //    case 0x927C: //"Maker Note"
                                    //        ProcessMakerNote(p.Value);
                                    //        break;
                                    //}
                                }
                                else
                                    if (p.Type == 0x9)
                                    {
                                        //v = convertToInt32(p.Value).ToString();//9 = SLONG A 32-bit (4 -byte) signed integer (2's complement notation),
                                        //switch (p.Id)
                                        //{

                                        //}
                                    }
                                    else
                                        if (p.Type == 0xA)//10 = SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
                                        {
                                            byte[] n = new byte[p.Len / 2];
                                            byte[] d = new byte[p.Len / 2];
                                            Array.Copy(p.Value, 0, n, 0, p.Len / 2);
                                            Array.Copy(p.Value, p.Len / 2, d, 0, p.Len / 2);
                                            int a = convertToInt32(n);
                                            int b = convertToInt32(d);
                                            Rational r = new Rational(a, b);

                                            switch (p.Id)
                                            {
                                                case 0x9203: // Brightness
                                                    m_brightness = (float)Math.Round(r.ToDouble(), 4);
                                                    break;
                                                case 0x9204: // Exposure bias(compensation)
                                                    m_exposureCompensation = b;
                                                    break;
                                                case 0x9201: // shutter speed
                                                    //  m_exposureTime = r.ToString(null);
                                                    //   string str = "1/" + Math.Round(Math.Pow(2, r.ToDouble()), 2).ToString();
                                                    break;
                                            }
                                        }
                                        else
                                        {
                                        }
            }
        }

        private DateTime GetDate(string stringValue)
        {
            DateTimeFormatInfo dti = new DateTimeFormatInfo();
            DateTime result = DateTime.MinValue;

            try
            {
                if (stringValue.Length == 19 &&
                    string.Compare(stringValue, "0000:00:00 00:00:00") != 0)
                {
                    if (stringValue[10] == ' ')
                    {
                        result = DateTime.ParseExact(stringValue, "yyyy:MM:dd HH:mm:ss", dti);
                    }
                    else
                    {
                        result = DateTime.ParseExact(stringValue, "yyyy:MM:dd:HH:mm:ss", dti);
                    }
                }
                else
                {
                    String[] dateParts = stringValue.Trim().Split(new String[] { ":", " " }, StringSplitOptions.RemoveEmptyEntries);

                    if (dateParts.Length == 6)
                    {
                        int year = int.Parse(dateParts[0]);
                        int month = int.Parse(dateParts[1]);
                        int day = int.Parse(dateParts[2]);
                        int hour = int.Parse(dateParts[3]);
                        int minute = int.Parse(dateParts[4]);
                        int second = int.Parse(dateParts[5].Substring(0, Math.Min(2, dateParts[5].Length)));

                        if (year != 0 && month != 0 && day != 0)
                        {
                            result = new DateTime(year, month, day, hour, minute, second);
                        }
                    }
                }
            }
            catch (FormatException)
            {
                result = DateTime.MinValue;
            }
            catch (ArgumentOutOfRangeException)
            {
                result = DateTime.MinValue;
            }
            catch (Exception ex)
            {
                result = DateTime.MinValue;
                Logger.Log(ex);
            }

            return result;
        }

        private void ProcessMakerNote(byte[] p)
        {
            //try
            //{
            //    //byte[] mltMakerNoteVersion = new byte[4];
            //    //Array.Copy(p, 10, mltMakerNoteVersion, 0, 4);
            //    String minoltaTag = ASCIIEncoding.ASCII.GetString(p);
            //    System.Diagnostics.Debug.Print(minoltaTag);
            //}
            //catch (Exception ex)
            //{
            //}
            //if (minoltaTag.CompareTo("MLT0") == 0)
            //{

            //}
        }

        internal static int convertToInt32(byte[] arr)
        {
            if (arr.Length != 4)
            {
                return 0;
            }
            else
            {
                return arr[3] << 24 | arr[2] << 16 | arr[1] << 8 | arr[0];
            }
        }

        //internal static int convertToInt16(byte[] arr)
        //{
        //    if (arr.Length != 2)
        //    {
        //        return 0;
        //    }
        //    else
        //    {
        //        return arr[1] << 8 | arr[0];
        //    }
        //}

        internal static uint convertToInt32U(byte[] arr)
        {
            if (arr.Length != 4)
            {
                return 0;
            }
            else
            {
                return Convert.ToUInt32(arr[3] << 24 | arr[2] << 16 | arr[1] << 8 | arr[0]);
            }
        }

        internal static uint convertToInt16U(byte[] arr)
        {
            if (arr.Length != 2)
            {
                return 0;
            }
            else
            {
                return Convert.ToUInt16(arr[1] << 8 | arr[0]);
            }
        }

        public void Save(string fileName)
        {
            try
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    using (Image image = Image.FromFile(fileName))
                    {
                        setTag(image, 0x9003, m_dateTaken);
                        setTag(image, 0x9004, m_dateDigitized);
                        setTag(image, 0x10E, string.IsNullOrEmpty(m_imageDescription) ? string.Empty : m_imageDescription);
                        //setTag(image, 0x131, string.IsNullOrEmpty(m_softwareUsed) ? "MyPhotoIndex" : m_softwareUsed); 

                        try
                        {
                            image.RemovePropertyItem(20507);
                        }
                        catch { }
                        if (m_thumbnail != null)
                        {
                            try
                            {
                                using (Image origThumbnail = ThumbnailUtilities.ThumbnailFromByteArray(m_thumbnail))
                                {
                                    Size calculatedSize = ImageAlgo.GetImageProportionalSize(new SizeF(origThumbnail.Width, origThumbnail.Height), new SizeF(240, 160));
                                    using (Image thumbnail = new Bitmap(origThumbnail, calculatedSize.Width, calculatedSize.Height))
                                    {
                                        byte[] thumbnailData = ThumbnailUtilities.ThumbnailToByteArray(thumbnail);
                                        setTag(image, 20507, thumbnailData.Length, 0x1, thumbnailData);
                                    }
                                }
                            }
                            catch { }
                        }

                        ImageCodecInfo codecInfo = GetEncoderInfo(Path.GetExtension(fileName));
                        image.Save(ms, codecInfo, null);
                    }

                    using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate | FileMode.Truncate, FileAccess.ReadWrite))
                    {
                        byte[] data = new byte[ms.Length];
                        ms.Position = 0;
                        int i = ms.Read(data, 0, (int)ms.Length);
                        fs.Write(data, 0, data.Length);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }
        }

        public static ImageCodecInfo GetEncoderInfo(String mimeType)
        {
            ImageCodecInfo[] encoders;
            encoders = ImageCodecInfo.GetImageEncoders();
            for (int j = 0; j < encoders.Length; ++j)
            {
                if (encoders[j].FilenameExtension.ToLower().IndexOf(mimeType.ToLower()) > -1)
                {
                    return encoders[j];
                }
            }

            return null;
        }

        public void setTag(Image image, int id, string data)
        {
            Encoding ascii = Encoding.ASCII;
            this.setTag(image, id, data.Length, 0x2, ascii.GetBytes(data));
        }

        public void setTag(Image image, int id, DateTime data)
        {
            // "yyyy:MM:dd HH:mm:ss"
            Object[] parameters = new Object[] { data.Year, data.Month, data.Day, data.Hour, data.Minute, data.Second };
            string stringData = string.Format("{0}:{1:00}:{2:00} {3:00}:{4:00}:{5:00}", parameters);

            this.setTag(image, id, stringData);
        }

        public void setTag(Image image, int id, int len, short type, byte[] data)
        {
            PropertyItem p = CreatePropertyItem(type, id, len, data);
            image.SetPropertyItem(p);
        }

        private static PropertyItem CreatePropertyItem(short type, int tag, int len, byte[] value)
        {
            PropertyItem item = Resources.exif.PropertyItems[0];

            if (item != null)
            {
                // Copies the data to the property item.
                item.Type = type;
                item.Len = len;
                item.Id = tag;
                item.Value = new byte[value.Length];
                value.CopyTo(item.Value, 0);
            }

            return item;
        }
    }
}
